/*
* (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Stephane Lacoin
*/
package org.nuxeo.runtime.datasource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Logger;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
import javax.resource.NotSupportedException;
import javax.resource.ResourceException;
import javax.resource.spi.ConnectionManager;
import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.InvalidPropertyException;
import javax.resource.spi.LocalTransaction;
import javax.resource.spi.LocalTransactionException;
import javax.resource.spi.ManagedConnection;
import javax.resource.spi.ManagedConnectionFactory;
import javax.resource.spi.ManagedConnectionMetaData;
import javax.resource.spi.ResourceAdapterInternalException;
import javax.resource.spi.ResourceAllocationException;
import javax.security.auth.Subject;
import javax.sql.DataSource;
import javax.sql.XADataSource;
import javax.transaction.xa.XAResource;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.runtime.datasource.PooledDataSourceRegistry.PooledDataSource;
import org.nuxeo.runtime.jtajca.NuxeoConnectionManagerConfiguration;
import org.nuxeo.runtime.jtajca.NuxeoConnectionManagerFactory;
import org.nuxeo.runtime.jtajca.NuxeoContainer;
import org.nuxeo.runtime.jtajca.NuxeoContainer.ConnectionManagerWrapper;
import org.nuxeo.runtime.transaction.TransactionHelper;
import org.tranql.connector.AbstractManagedConnection;
import org.tranql.connector.CredentialExtractor;
import org.tranql.connector.ExceptionSorter;
import org.tranql.connector.ManagedConnectionHandle;
import org.tranql.connector.UserPasswordManagedConnectionFactory;
import org.tranql.connector.jdbc.AutocommitSpecCompliant;
import org.tranql.connector.jdbc.ConnectionHandle;
import org.tranql.connector.jdbc.KnownSQLStateExceptionSorter;
import org.tranql.connector.jdbc.LocalDataSourceWrapper;
import org.tranql.connector.jdbc.TranqlDataSource;
import org.tranql.connector.jdbc.XADataSourceWrapper;
public class PooledDataSourceFactory implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> environment) {
class NuxeoDataSource extends TranqlDataSource implements PooledDataSource {
protected ConnectionManagerWrapper wrapper;
public NuxeoDataSource(ManagedConnectionFactory mcf, ConnectionManagerWrapper wrapper) {
super(mcf, wrapper);
this.wrapper = wrapper;
}
@Override
public void dispose() {
wrapper.dispose();
}
@Override
public Connection getConnection(boolean noSharing) throws SQLException {
if (!noSharing) {
return getConnection();
}
wrapper.getManager().enterNoSharing();
try {
return getConnection();
} finally {
wrapper.getManager().exitNoSharing();
}
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException("not yet available");
}
}
Reference ref = (Reference) obj;
ManagedConnectionFactory mcf;
ConnectionManagerWrapper cm;
try {
mcf = createFactory(ref, ctx);
cm = createManager(ref, ctx);
} catch (ResourceException | NamingException e) {
throw new RuntimeException(e);
}
return new NuxeoDataSource(mcf, cm);
}
protected ConnectionManagerWrapper createManager(Reference ref, Context ctx) throws ResourceException {
NuxeoConnectionManagerConfiguration config = NuxeoConnectionManagerFactory.getConfig(ref);
String className = ref.getClassName();
config.setXAMode(XADataSource.class.getName().equals(className));
return NuxeoContainer.initConnectionManager(config);
}
protected ManagedConnectionFactory createFactory(Reference ref, Context ctx) throws NamingException,
InvalidPropertyException {
String className = ref.getClassName();
if (XADataSource.class.getName().equals(className)) {
String user = refAttribute(ref, "User", "");
String password = refAttribute(ref, "Password", "");
String name = refAttribute(ref, "dataSourceJNDI", null);
XADataSource ds = NuxeoContainer.lookup(name, XADataSource.class);
XADataSourceWrapper wrapper = new XADataSourceWrapper(ds);
wrapper.setUserName(user);
wrapper.setPassword(password);
return wrapper;
}
if (javax.sql.DataSource.class.getName().equals(className)) {
String user = refAttribute(ref, "username", "");
if (user.isEmpty()) {
user = refAttribute(ref, "user", "");
if (!user.isEmpty()) {
LogFactory.getLog(PooledDataSourceFactory.class).warn(
"wrong attribute 'user' in datasource descriptor, should use 'username' instead");
}
}
String password = refAttribute(ref, "password", "");
String dsname = refAttribute(ref, "dataSourceJNDI", "");
if (!dsname.isEmpty()) {
javax.sql.DataSource ds = NuxeoContainer.lookup(dsname, DataSource.class);
LocalDataSourceWrapper wrapper = new LocalDataSourceWrapper(ds);
wrapper.setUserName(user);
wrapper.setPassword(password);
return wrapper;
}
String name = refAttribute(ref, "driverClassName", null);
String url = refAttribute(ref, "url", null);
String sqlExceptionSorter = refAttribute(ref, "sqlExceptionSorter",
DatasourceExceptionSorter.class.getName());
boolean commitBeforeAutocommit = Boolean.valueOf(refAttribute(ref, "commitBeforeAutocommit", "true")).booleanValue();
JdbcConnectionFactory factory = new JdbcConnectionFactory();
factory.setDriver(name);
factory.setUserName(user);
factory.setPassword(password);
factory.setConnectionURL(url);
factory.setExceptionSorterClass(sqlExceptionSorter);
factory.setCommitBeforeAutocommit(commitBeforeAutocommit);
return factory;
}
throw new IllegalArgumentException("unsupported class " + className);
}
static class JdbcConnectionFactory implements UserPasswordManagedConnectionFactory, AutocommitSpecCompliant {
private static final long serialVersionUID = 4317141492511322929L;
private Driver driver;
private String url;
private String user;
private String password;
private ExceptionSorter exceptionSorter = new KnownSQLStateExceptionSorter();
private boolean commitBeforeAutocommit = false;
private PrintWriter log;
@Override
public Object createConnectionFactory() throws ResourceException {
throw new NotSupportedException("ConnectionManager is required");
}
@Override
public Object createConnectionFactory(ConnectionManager connectionManager) throws ResourceException {
return new TranqlDataSource(this, connectionManager);
}
@Override
public ManagedConnection createManagedConnection(Subject subject, ConnectionRequestInfo connectionRequestInfo) throws ResourceException {
class ManagedJDBCConnection extends AbstractManagedConnection<Connection, ConnectionHandle> {
final CredentialExtractor credentialExtractor;
final LocalTransactionImpl localTx;
final LocalTransactionImpl localClientTx;
final boolean commitBeforeAutoCommit;
Exception fatalError;
ManagedJDBCConnection(UserPasswordManagedConnectionFactory mcf, Connection physicalConnection,
CredentialExtractor credentialExtractor, ExceptionSorter exceptionSorter, boolean commitBeforeAutoCommit) {
super(mcf, physicalConnection, exceptionSorter);
this.credentialExtractor = credentialExtractor;
localTx = new LocalTransactionImpl(true);
localClientTx = new LocalTransactionImpl(false);
this.commitBeforeAutoCommit = commitBeforeAutoCommit;
}
@Override
public boolean matches(ManagedConnectionFactory mcf, Subject subject, ConnectionRequestInfo connectionRequestInfo)
throws ResourceAdapterInternalException {
return credentialExtractor.matches(subject, connectionRequestInfo, (UserPasswordManagedConnectionFactory) mcf);
}
@Override
public LocalTransaction getClientLocalTransaction() {
return localClientTx;
}
@Override
public LocalTransaction getLocalTransaction() throws ResourceException {
return localTx;
}
Connection physicalConnection() throws ResourceException {
return physicalConnection;
}
@Override
protected void localTransactionStart(boolean isSPI) throws ResourceException {
Connection c = physicalConnection();
try {
c.setAutoCommit(false);
} catch (SQLException e) {
throw new LocalTransactionException("Unable to disable autoCommit", e);
}
super.localTransactionStart(isSPI);
}
@Override
protected void localTransactionCommit(boolean isSPI) throws ResourceException {
Connection c = physicalConnection();
try {
if (commitBeforeAutoCommit) {
c.commit();
}
} catch (SQLException e) {
try {
c.rollback();
} catch (SQLException e1) {
if (log != null) {
e.printStackTrace(log);
}
}
throw new LocalTransactionException("Unable to commit", e);
} finally {
try {
c.setAutoCommit(true);
} catch (SQLException e) {
throw new ResourceAdapterInternalException("Unable to enable autoCommit after rollback", e);
}
}
super.localTransactionCommit(isSPI);
}
@Override
protected void localTransactionRollback(boolean isSPI) throws ResourceException {
Connection c = physicalConnection;
try {
c.rollback();
} catch (SQLException e) {
throw new LocalTransactionException("Unable to rollback", e);
}
super.localTransactionRollback(isSPI);
try {
c.setAutoCommit(true);
} catch (SQLException e) {
throw new ResourceAdapterInternalException("Unable to enable autoCommit after rollback", e);
}
}
@Override
public XAResource getXAResource() throws ResourceException {
throw new NotSupportedException("XAResource not available from a LocalTransaction connection");
}
@Override
protected void closePhysicalConnection() throws ResourceException {
Connection c = physicalConnection;
try {
c.close();
} catch (SQLException e) {
throw new ResourceAdapterInternalException("Error attempting to destroy managed connection", e);
}
}
@Override
public ManagedConnectionMetaData getMetaData() throws ResourceException {
throw new NotSupportedException("no metadata available yet");
}
@Override
public void connectionError(Exception e) {
if (fatalError != null) {
return;
}
if (isFatal(e)) {
fatalError = e;
if (exceptionSorter.rollbackOnFatalException()) {
if (TransactionHelper.isTransactionActive()) {
// will roll-back at tx end through #localTransactionRollback
TransactionHelper.setTransactionRollbackOnly();
} else {
attemptRollback();
}
}
}
}
@Override
public void cleanup() throws ResourceException {
super.cleanup();
if (fatalError != null) {
ResourceException error = new ResourceException(String.format("fatal error occurred on %s, destroying", this), fatalError);
LogFactory.getLog(ManagedJDBCConnection.class).warn(error.getMessage(), error.getCause());
throw error;
}
}
protected boolean isFatal(Exception e) {
if (exceptionSorter.isExceptionFatal(e)) {
return true;
}
try {
return !physicalConnection.isValid(10);
} catch (SQLException cause) {
return false; // could not state
} catch (LinkageError cause) {
return false; // not compliant JDBC4 driver
}
}
@Override
protected void attemptRollback() {
try {
physicalConnection.rollback();
} catch (SQLException e) {
// ignore.... presumably the connection is actually dead
}
}
@Override
public String toString() {
return super.toString() + ". jdbc=" + physicalConnection;
}
}
CredentialExtractor credentialExtractor = new CredentialExtractor(subject, connectionRequestInfo, this);
Connection sqlConnection = getPhysicalConnection(subject, credentialExtractor);
return new ManagedJDBCConnection(this, sqlConnection, credentialExtractor, exceptionSorter, commitBeforeAutocommit);
}
protected Connection getPhysicalConnection(Subject subject, CredentialExtractor credentialExtractor) throws ResourceException {
try {
if (!driver.acceptsURL(url)) {
throw new ResourceAdapterInternalException("JDBC Driver cannot handle url: " + url);
}
} catch (SQLException e) {
throw new ResourceAdapterInternalException("JDBC Driver rejected url: " + url);
}
Properties info = new Properties();
String user = credentialExtractor.getUserName();
if (user != null) {
info.setProperty("user", user);
}
String password = credentialExtractor.getPassword();
if (password != null) {
info.setProperty("password", password);
}
try {
return driver.connect(url, info);
} catch (SQLException e) {
throw new ResourceAllocationException("Unable to obtain physical connection to " + url, e);
}
}
@Override
public ManagedConnection matchManagedConnections(@SuppressWarnings("rawtypes") Set set, Subject subject, ConnectionRequestInfo connectionRequestInfo)
throws ResourceException {
for (@SuppressWarnings("unchecked") Iterator<Object> i = set.iterator(); i.hasNext();) {
Object o = i.next();
if (o instanceof ManagedConnectionHandle) {
ManagedConnectionHandle<?,?> mc = (ManagedConnectionHandle<?,?>) o;
if (mc.matches(this, subject, connectionRequestInfo)) {
return mc;
}
}
}
return null;
}
@Override
public PrintWriter getLogWriter() {
return log;
}
@Override
public void setLogWriter(PrintWriter log) {
this.log = log;
}
void setDriver(String driver) throws InvalidPropertyException {
if (driver == null || driver.length() == 0) {
throw new InvalidPropertyException("Empty driver class name");
}
try {
@SuppressWarnings("unchecked")
Class<Driver> driverClass = (Class<Driver>) Class.forName(driver);
this.driver = driverClass.newInstance();
} catch (ClassNotFoundException e) {
throw new InvalidPropertyException("Unable to load driver class: " + driver, e);
} catch (InstantiationException e) {
throw new InvalidPropertyException("Unable to instantiate driver class: " + driver, e);
} catch (IllegalAccessException e) {
throw new InvalidPropertyException("Unable to instantiate driver class: " + driver, e);
} catch (ClassCastException e) {
throw new InvalidPropertyException("Class is not a " + Driver.class.getName() + ": " + driver, e);
}
}
void setConnectionURL(String url) throws InvalidPropertyException {
if (url == null || url.length() == 0) {
throw new InvalidPropertyException("Empty connection URL");
}
this.url = url;
}
@Override
public String getUserName() {
return user;
}
void setUserName(String user) {
this.user = user;
}
@Override
public String getPassword() {
return password;
}
void setPassword(String password) {
this.password = password;
}
@Override
public Boolean isCommitBeforeAutocommit() {
return Boolean.valueOf(commitBeforeAutocommit);
}
void setCommitBeforeAutocommit(Boolean commitBeforeAutocommit) {
this.commitBeforeAutocommit = commitBeforeAutocommit != null && commitBeforeAutocommit.booleanValue();
}
void setExceptionSorterClass(String className) throws InvalidPropertyException {
if (className == null || className.length() == 0) {
throw new InvalidPropertyException("Empty class name");
}
try {
@SuppressWarnings("unchecked")
Class<ExceptionSorter> clazz = (Class<ExceptionSorter>) Class.forName(className);
exceptionSorter = clazz.newInstance();
} catch (ClassNotFoundException e) {
throw new InvalidPropertyException("Unable to load class: " + className, e);
} catch (IllegalAccessException e) {
throw new InvalidPropertyException("Unable to instantiate class: " + className, e);
} catch (InstantiationException e) {
throw new InvalidPropertyException("Unable to instantiate class: " + className, e);
} catch (ClassCastException e) {
throw new InvalidPropertyException("Class is not a " + ExceptionSorter.class.getName() + ": " + driver, e);
}
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof JdbcConnectionFactory) {
JdbcConnectionFactory other = (JdbcConnectionFactory) obj;
return url == other.url || url != null && url.equals(other.url);
}
return false;
}
@Override
public int hashCode() {
return url == null ? 0 : url.hashCode();
}
@Override
public String toString() {
return "Pooled JDBC Driver Connection Factory [" + user + "@" + url + "]";
}
}
protected String refAttribute(Reference ref, String key, String defvalue) {
RefAddr addr = ref.get(key);
if (addr == null) {
if (defvalue == null) {
throw new IllegalArgumentException(key + " address is mandatory");
}
return defvalue;
}
return (String) addr.getContent();
}
}